import Level from 'level-ts'
import { isError } from 'lodash'
import { BigNumber } from 'bignumber.js'

import { KvDB } from '@app/kvdb'
import { Wallet } from '@app/wallet'
import { logger } from '@pkg/logger'
import { sleep, nothrow } from '@pkg/promise'

class BalanceSheet {
    private queue: KvDB
    private balances: KvDB
    private wallet: Wallet
    private started = false
    readonly symbol: string

    constructor(wallet: Wallet, database: Level, symbol: string) {
        this.wallet = wallet
        this.symbol = symbol
        this.queue = new KvDB(database, symbol)
        this.balances = new KvDB(database, 'balances::' + symbol)
        this.startPolling()
    }

    /**
     * 开始轮询地址队列
     * 查询并更新地址队列中每个地址的余额
     */
    private async startPolling() {
        if (this.started) {
            return
        }

        this.started = true
        while (this.started) {
            const item = await this.queue.pop()
            if (!item) {
                await sleep(1000)
                continue
            }

            const { key } = item
            const value = await nothrow(this.wallet.getBalance(key, this.symbol))
            if (isError(value)) {
                await this.queue.add(key)
                await sleep(1000 * 5)
                continue
            }

            const balance = new BigNumber(value)
            if (balance.isZero()) {
                await this.balances.remove(key)
            } else {
                await this.balances.add(key, value)
            }
            logger.debug('[BalanceSheet] balance updated, address: %s, symbol: %s, balance: %s',
                key, this.symbol, value)
        }
    }

    /**
     * 获取地址余额
     * @param address - 地址
     */
    async getBalance(address: string) {
        const value = await this.balances.get(address)
        if (!value) {
            return new BigNumber(0)
        }
        return new BigNumber(value)
    }

    /**
     * 更新地址余额缓存
     * @param address - 地址
     */
    async updateBalance(address: string) {
        return this.queue.add(address)
    }

    /**
     * 遍历本地地址余额
     * @param callbackfn - 回调函数，接收address和balance，如果需要继续遍历则返回true
     */
    async forEach(callbackfn: (key: string, balance: BigNumber) => Promise<boolean>) {
        await this.balances.forEach((address: string, value: any) =>  {
            return callbackfn(address, new BigNumber(value))
        })
    }
}

class BalanceManager {
    private wallet: Wallet
    private database: Level
    private balanceSheets: Map<string, BalanceSheet>

    constructor(wallet: Wallet, database: Level) {
        const self = this
        this.wallet = wallet
        this.database = database
        this.balanceSheets = new Map<string, BalanceSheet>();
        (async () => {
            const symbols = self.wallet.getSymbols()
            for (const idx in symbols) {
                const symbol = symbols[idx]
                if (!self.balanceSheets.has(symbol)) {
                    self.balanceSheets.set(symbol, new BalanceSheet(self.wallet, database, symbol))
                }
            }
        })()
    }

    /**
     * 获取资产负债表
     * @param symbol - 代币符号
     */
    getBalanceSheet(symbol: string) {
        return this.balanceSheets.get(symbol)
    }

    /**
     * 获取地址余额
     * @param address - 地址
     * @param symbol - 代币符号
     */
    async getBalance(address: string, symbol: string) {
        let balanceSheet = this.balanceSheets.get(symbol)
        if (!balanceSheet) {
            return new BigNumber(0)
        }
        return balanceSheet.getBalance(address)
    }

    /**
     * 更新地址余额缓存
     * @param address - 地址
     * @param symbol - 代币符号
     */
    async updateBalance(address: string, symbol: string) {
        let balanceSheet = this.balanceSheets.get(symbol)
        if (!balanceSheet) {
            balanceSheet = new BalanceSheet(this.wallet, this.database, symbol)
            this.balanceSheets.set(symbol, balanceSheet)
        }
        return balanceSheet.updateBalance(address)
    }
}

export { BalanceManager, BalanceSheet }
